WIP: Add gh-style plugin system (skeleton + dispatch + local install)#1116
WIP: Add gh-style plugin system (skeleton + dispatch + local install)#1116
Conversation
Implements the foundational milestones from docs/plugin-system-plan.md. External executables named entire-<name> are discovered under $ENTIRE_PLUGIN_DIR (or XDG_DATA_HOME/entire/plugins) and dispatched to when an unknown subcommand is invoked. Cobra resolves built-ins first; 'entire plugin exec <name>' is the escape hatch for collisions. Local install (symlinking a dev directory), list, remove, and exec are wired up. GitHub-release binary install, git-clone script install, upgrade, create scaffolds, search/browse/pin, and the update notifier remain deferred per the plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: eb3926f7bf64
There was a problem hiding this comment.
Pull request overview
This PR introduces a foundational, gh-style plugin system for the Entire CLI: plugins are discovered from a user plugin directory and can be dispatched to when the user invokes an unknown subcommand, plus a new entire plugin command group for basic lifecycle operations.
Changes:
- Add a new
cmd/entire/cli/pluginpackage implementing plugin discovery/classification, local install (symlink), removal, manifest support, and cross-platform dispatch (includingsh.exerouting for Windows script plugins). - Wire
entire plugin {install,list,remove,exec}into the Cobra root, and hook plugin dispatch intomain.go’s unknown-command path. - Add initial unit tests covering manager classification, local install/remove, and dispatch behavior (including exit-code propagation in the implicit-dispatch path).
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| go.mod | Add YAML dependency for binary plugin manifests. |
| docs/plugin-system-plan.md | Document intended architecture/milestones for the plugin system. |
| cmd/entire/main.go | Attempt plugin dispatch before unknown-command/flag suggestion handling. |
| cmd/entire/cli/root.go | Register new top-level plugin command group. |
| cmd/entire/cli/plugin_group.go | Implement entire plugin install/list/remove/exec commands. |
| cmd/entire/cli/plugin/manager.go | Implement plugin root resolution, discovery, classification, and name validation. |
| cmd/entire/cli/plugin/manifest.go | Define/read/write YAML manifest for binary plugins. |
| cmd/entire/cli/plugin/dispatch.go | Implement dispatch resolution and execution, env injection, and exit-code extraction. |
| cmd/entire/cli/plugin/dispatch_unix.go | Unix exec implementation (direct). |
| cmd/entire/cli/plugin/dispatch_windows.go | Windows exec implementation (script routing via sh.exe). |
| cmd/entire/cli/plugin/install_local.go | Implement local install via symlink with built-in conflict checks. |
| cmd/entire/cli/plugin/remove.go | Implement plugin removal semantics (unlink local vs remove dir). |
| cmd/entire/cli/plugin/pin.go | Implement pin marker detection helper. |
| cmd/entire/cli/plugin/manager_test.go | Add tests for name validation, default root env override, and classification. |
| cmd/entire/cli/plugin/install_test.go | Add tests for local install behavior and remove semantics. |
| cmd/entire/cli/plugin/dispatch_test.go | Add tests for dispatch fallthrough, precedence, env injection, and exit-code propagation. |
| case strings.Contains(err.Error(), "unknown command") || strings.Contains(err.Error(), "unknown flag"): | ||
| handled, pluginErr := plugin.Dispatch(ctx, rootCmd, os.Args[1:]) | ||
| if handled { |
| if err := plugin.Exec(cmd.Context(), p, rest, mgr.Root); err != nil { | ||
| code := plugin.PropagateExitCode(err) | ||
| if code > 0 { | ||
| // Plugin returned a non-zero exit code. Surface it via a | ||
| // SilentError so main.go preserves the user's intent | ||
| // without printing extra noise. | ||
| return NewSilentError(errors.New(p.FullName() + " exited with non-zero status")) | ||
| } | ||
| return fmt.Errorf("exec plugin: %w", err) |
| continue | ||
| } | ||
| p, err := m.classify(name) | ||
| if err != nil || p == nil { | ||
| continue | ||
| } |
| func readPinSHA(dir string) string { | ||
| entries, err := os.ReadDir(dir) | ||
| if err != nil { | ||
| return "" | ||
| } |
| func pluginDataDir(root, name string) string { | ||
| return root + string(os.PathSeparator) + Prefix + name + string(os.PathSeparator) + "data" | ||
| } |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 7376f07. Configure here.
| // without printing extra noise. | ||
| return NewSilentError(errors.New(p.FullName() + " exited with non-zero status")) | ||
| } | ||
| return fmt.Errorf("exec plugin: %w", err) |
There was a problem hiding this comment.
plugin exec loses child's non-zero exit code
High Severity
When entire plugin exec runs a plugin that exits non-zero (e.g., 42), PropagateExitCode correctly extracts the code, but NewSilentError(errors.New(...)) wraps a new error that discards the original *exec.ExitError. In main.go, the SilentError case simply skips printing and falls through to the unconditional os.Exit(1), so the child's actual exit code is always replaced with 1. The dispatch path in main.go correctly calls os.Exit(code), but the entire plugin exec cobra subcommand path does not — contradicting the stated "child exits 42 → parent exits 42" guarantee.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7376f07. Configure here.
|
Closing in favor of #1104, which is the right starting point. The kubectl-style PATH dispatcher there is lower-friction, ships now, and migrates cleanly to a managed-install layer later — exactly what the plan doc anticipated. I'll repurpose this branch as a follow-up that builds additively on top of #1104, cherry-picking only the parts that are missing there:
Deferring (per discussion):
|


https://entire.io/gh/entireio/cli/trails/298
Summary
cmd/entire/cli/pluginpackage with manager/classification (binary/script/local), YAML manifest, dispatcher (Unix direct exec, Windowssh.exefor scripts), local symlink install, and remove.entire plugin {install,list,remove,exec}subcommands wired into root.main.gonow callsplugin.Dispatchbefore the unknown-command suggestion path; cobra resolves built-ins first, so plugins can never shadow built-ins.plugin execis the escape hatch.Foundational milestones from
docs/plugin-system-plan.md. Network-dependent paths (GitHub release binary install, git-clone script install, upgrade, create scaffolds, search/browse/pin, update notifier, docs page) are deferred.Test plan
mise run lintpasses (0 issues)mise run testpassesmise run test:integrationpassesmise run test:e2e:canarypassesentire statusresolves to built-in even whenentire-statusplugin is installed;entire plugin exec statusinvokes the pluginentire plugin installrefuses a directory whose name matches a built-in🤖 Generated with Claude Code
Note
Medium Risk
Adds a new plugin execution path for unknown commands and introduces filesystem-based install/remove/list behavior, which can affect CLI command resolution and process exit codes. Main risk is dispatch/exec edge cases across platforms (Windows
sh.exerouting) and potential regressions in unknown-command handling.Overview
Adds an initial gh-style plugin system: a new
pluginpackage can discover installed plugins (binary/script/local), parse/writemanifest.yml, track pins via.pin-*, and execute plugins with stdio inheritance,ENTIRE_PLUGIN_DATA_DIRinjection, and exit-code propagation (including Windowssh.exerouting for script plugins).Wires new
entire plugin {install,list,remove,exec}commands into the CLI and updatesmain.goto attemptplugin.Dispatchon unknown commands/flags before showing suggestions, while ensuring built-in Cobra commands/aliases always take precedence. Includes unit tests covering classification, local install via symlink, remove semantics, dispatch precedence, argv handling, env injection, and exit-code propagation, plus addsgopkg.in/yaml.v3dependency.Reviewed by Cursor Bugbot for commit 7376f07. Configure here.